feat(tables): add select & multi-select column types#5873
feat(tables): add select & multi-select column types#5873TheodoreSpeaks wants to merge 35 commits into
Conversation
Adds two enum-style column types where the column declares a fixed set of options (stable id + name + palette color) and every cell is constrained to them. - COLUMN_TYPES gains `select` / `multiselect`; SELECT_COLORS is a fixed, theme-aware palette mapping 1:1 to Badge color variants (no raw hex) - ColumnDefinition.options carries the option set. Cells store option *ids* (a string for select, string[] for multiselect) so renaming or recoloring an option never rewrites row data - Row validation enforces membership; coercion tolerantly maps an option *name* to its id for tool/import writes and drops unmatched entries - validateColumnDefinition enforces non-empty, unique-id, unique-name and valid-color option sets, and rejects options on non-select columns - Contract gains selectOptionSchema plus a cross-field refine requiring options exactly on select types; routes thread options through type changes and a new options-only updateColumnOptions path No migration: column config already lives in the user_table_definitions schema JSONB blob. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
Surfaces the new enum column types in the tables grid. - New `select-field/` module: SelectPill (colored option via the shared Badge palette), SelectValueEditor (one ChipDropdown-backed picker reused by every edit surface), SelectOptionsEditor (add/rename/recolor/remove) - Option colors are picked from inline squircle swatches — no labels, no nested dropdown. Each swatch's fill is a Badge in that variant, so the palette stays single-sourced and theme-aware - Cells render option pills; an empty select cell shows a muted "None" so it reads as a dropdown. The single-select menu always offers "None" to clear - Wired into all three edit surfaces (inline cell, expanded popover, row modal) plus the type picker and column-type icons - ChipDropdown gains `defaultOpen`/`onOpenChange` so the inline cell editor can open on mount and commit when the menu closes; open state is now controlled in both single and multi modes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
Column auto-resize measured `String(val)` for every non-json/date column, which for a select cell is the opaque option id (and for multiselect the comma-joined id array). Selecting auto-fit on a select column therefore sized it to ids the user never sees. Measure the resolved option names instead, matching what the pills actually render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
Removes the per-option swatch picker and defaults every option to the neutral gray pill. The `color` field stays in the data model and the SELECT_COLORS palette/contract are untouched, so a picker can be re-added later as a pure UI change with no migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Server & data path: New UI: Column sidebar configures options and multiselect (unique hidden for select); grid inline dropdown, row modal, filters, pills, copy/paste, and undo restore options on column delete. Integrations: V1/copilot table tools, contracts, and catalog docs gain Reviewed by Cursor Bugbot for commit 75d514b. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds select and multiselect columns to Tables. The main changes are:
Confidence Score: 5/5This looks safe to merge.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/table/columns/service.ts | Validates existing values before select conversion and migrates compatible values to option IDs. |
| apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx | Prevents required single-select and multiselect fields from being cleared. |
Reviews (12): Last reviewed commit: "fix(tables): clear unique in the service..." | Re-trigger Greptile
Addresses review findings on the select/multiselect column types: - Column type conversion now checks each existing value against the target option set (resolve by id or name); a `select`/`multiselect` change is blocked when values don't fit, instead of accepting shapes that later coercion would strand or silently drop - Inline select editor discards its draft on Escape (matching the text/date editors) rather than committing on menu close - A required single-select no longer offers "None" — clearing to null could never be committed Exports resolveSelectOptionId from validation for the conversion gate.
|
@cursor review |
Round 2 review fixes: - A required multiselect can no longer be emptied — the toggle that would remove the last option is ignored, since an empty selection can't be committed (server rejects it). Mirrors the required single-select "None" guard. - Inline cell save now compares old vs new value structurally, so a no-op edit (e.g. opening a multiselect and closing it unchanged, producing a new array reference) no longer writes a row update or pushes an undo entry. Uses JSON compare for arrays/objects, matching the existing optionsEqual convention; primitives keep the === fast path.
|
@cursor review |
Round 3 review fixes: - Expanded-cell popover normalizes a multiselect's initial value with toSelectedIds, so a single option-id string (the normal shape right after a select->multiselect conversion, before the row is rewritten) is no longer collapsed to an empty array and cleared on save. - Column PATCH now only routes to updateColumnType on a real type change. updateColumnType early-returns on an unchanged type, so a payload repeating the current type together with new options previously applied neither; an unchanged type with options now routes to the options-only update. Fixed in both the internal and v1 column routes.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4db5e88. Configure here.
- Double-clicking a select/multiselect cell now opens the inline option dropdown (like date/number) instead of the fixed-height text popover, which rendered just a small dropdown floating in a large empty container. Removes the now-unreachable ExpandedSelectEditor from the expanded-cell popover. - Options editor's add/remove controls match the sibling Filter UI: a ghost, muted "Add option" button with a small plus, and an X remove icon.
|
@cursor review |
|
@cursor review |
Two spots where the check and the migration disagreed, each leaving a cell the check had already accounted for: - resolveSelectOptionId matches option names case-insensitively, so a cell like 'open' against option 'Open' passed the convert-to-select check, but the migration map was case-sensitive and left it as the raw name. The map now keys the folded name too (duplicate names are already rejected case-insensitively, so it is unambiguous) and lookups try the exact form first. - Converting away from select, the check treats an orphaned option id as null while the migration passed it through, so a column with deleted-option cells could land an opaque opt_ id in a number/date/boolean cell. Orphans now null, matching selectValueForConversion; a multi cell drops them and nulls when nothing survives. This reverses the pass-through choice from the earlier round — consistency with the check is what matters, and an orphan is a value the UI never rendered.
|
@cursor review |
`[]` is the canonical empty multiselect but it is not falsy, so every emptiness check that only looked for null/undefined/'' read a cleared selection as filled: - dependent workflow groups became eligible off an empty cell, and enrichments with a required input ran on rows they should have skipped - marking a column required did not count `[]` rows, even though write-path validation rejects an empty required multiselect One `isEmptyCellValue` predicate now backs the dep, output-filled and enrichment checks, and the required-constraint guard counts '[]' too. Also normalizes '' on conversion to select — compatibility admits it, so it has to land as null (single) or [] (multi) rather than a bare string the column's own validation would then reject.
|
@cursor review |
updateColumnType early-returns when the type is unchanged, so an agent that restated newType: 'select' alongside a new option set had those options silently dropped while the tool still reported success. The HTTP columns route already guards this; the copilot path now mirrors it — only a genuine type change goes to updateColumnType, anything else with options or multiple goes to updateColumnOptions. A payload that only restates the current type is a no-op and now reports the live schema rather than an undefined one.
|
@cursor review |
…ion errors to 400 - The delete-column undo snapshot never captured options or multiple, so re-creating a select column was rejected outright (it is invalid with no option set) and the saved cell data — which is option ids — had nothing to attach to. Undo of a select column deletion simply could not succeed. - The v1 add-column route mapped only 'already exists' and 'maximum column' to 400, while the internal route also maps invalid-column and option errors. A bad select option set surfaced as a 500 on the public API.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e3bef6f. Configure here.
…select The clearing lived only in the column-config sidebar, so a conversion through the v1 API or the copilot tool left unique: true stranded on a select column — where the constraint compares the stored option id, capping each option at one row for the whole table, and the sidebar hides the toggle so it could never be cleared again. Moving it into updateColumnType gives all three callers the same behavior; the sidebar's payload is now belt-and-braces rather than the only enforcement.
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75d514b. Configure here.
| ) | ||
|
|
||
| return { ...table, schema: updatedSchema, updatedAt: now } | ||
| }) |
There was a problem hiding this comment.
Option removal leaves blocking orphans
High Severity
updateColumnOptions leaves cells holding removed option ids untouched. The grid drops those ids and shows None, but write-path validation still sees them: on a required select, coerceRowValues keeps the orphan and validateRowAgainstSchema rejects it, so unrelated edits on that row fail. Emptiness checks, is empty filters, and dependency satisfaction also treat the orphan as filled. The copilot catalog claims removed-option cells are cleared.
Reviewed by Cursor Bugbot for commit 75d514b. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75d514b. Configure here.
| return resolveSelectOptions(column, value) | ||
| .map((o) => o.name) | ||
| .join(', ') | ||
| } |
There was a problem hiding this comment.
Paste stores names, not option ids
High Severity
cellToText now copies select cells as option names, but cleanCellValue still returns the pasted string unchanged. Batch-update optimism writes that name into the row cache, where resolveSelectOptions looks up by id, so the cell renders as None. There is no post-success refetch to replace the optimistic name with the server-coerced id, so a copy/paste round-trip leaves select cells blank in the grid.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 75d514b. Configure here.
| } else if (column.options !== undefined) { | ||
| errors.push(`Column "${column.name}" cannot define options for type "${column.type}"`) | ||
| } | ||
|
|
There was a problem hiding this comment.
Unique allowed on select columns
Medium Severity
The feature treats select columns as non-unique — the sidebar hides the toggle and updateColumnType clears unique on conversion — but validateColumnDefinition and updateColumnConstraints never reject unique: true on a select column. An API or copilot write can set it, after which each option id is capped at one row table-wide and the UI has no control to clear the stranded constraint.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 75d514b. Configure here.


Summary
select(single) andmultiselectenum column types to Tables — a column declares a fixed set of options and every cell is constrained to them, rendered as pillscolorfield + full palette stay in the model/contract so a picker can be re-added later with no migrationChipDropdowngainsdefaultOpen/onOpenChangefor inline editingKnown limitations
options(follow-up)Type of Change
Testing
Tested manually.
bun run lintclean,check:api-validation:strictpassed,check:react-querypassed; 335 table + 14 emcn unit tests green (incl. newvalidation.test.tscovering membership, name→id coercion, and option-set validation)Checklist